#include <iostream>

#include <stdlib.h>
#include <inttypes.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <string.h>

#include <etherfabric/ef_vi.h>
#include <etherfabric/vi.h>
#include <etherfabric/pd.h>
#include <etherfabric/memreg.h>
#include <etherfabric/pio.h>
#include <etherfabric/checksum.h>
#include <etherfabric/capabilities.h>

#include "net_context.h"
#include "common_defs.h"

// using namespace usnet;

#define MEMBER_OFFSET(c_type, mbr_name) \
    ((uint32_t) (uintptr_t)(&((c_type*)0)->mbr_name))

using namespace usnet;

const char*     LOCAL_ADDR      { "10.1.1.7" };
const char*     REMOTE_ADDR     { "10.1.1.8" };
const uint16_t  LOCAL_PORT      { 6789 };
const uint16_t  REMOTE_PORT     { 6789 };
const char*     IFNAME          { "p2p1" };

ef_pd               pd;
ef_vi               vi;
ef_driver_handle    handle;
ef_memreg           memreg;

void* mem_ptr;
const int MEM_ALIGN { 4096 };
struct FrameBuffer {
    int             id_;
    ef_addr         dma_addr_;
    unsigned        mem_addr_[1] EF_VI_ALIGN(EF_VI_DMA_ALIGN);
};
struct FrameBuffer*     pkt_bufs[20];

int main(int argc, char* argv[]) {
    NetContext::Init();

    if (ef_driver_open(&handle) != 0) {
        std::cerr << "ef_driver_open() failed" << std::endl;
        return -1;
    }
    if (ef_pd_alloc(&pd, handle, if_nametoindex(IFNAME), EF_PD_DEFAULT) != 0) {
        std::cerr << "ef_pd_alloc() failed" << std::endl;
        return -1;
    }
    if (ef_vi_alloc_from_pd(
        &vi, handle,
        &pd, handle,
        20, 10, 10,
        nullptr, -1, EF_VI_FLAGS_DEFAULT) != 0) {
        std::cerr << "ef_vi_alloc_from_pd() failed" << std::endl;
        return -1;
    }
    const int nbytes { 20 * 2048 };
    if (posix_memalign(&mem_ptr, MEM_ALIGN, nbytes) != 0) {
        std::cerr << "posix_memalign() failed" << std::endl;
        return -1;
    }
    memset(mem_ptr, 0, nbytes);
    if (ef_memreg_alloc(
            &memreg, 
            handle,
            &pd,
            handle,
            mem_ptr,
            nbytes) != 0) {
        std::cerr << "ef_memreg_alloc() failed" << std::endl;
        return -1;
    }

    int fb_offset { 0 };
    const int payload_offset = MEMBER_OFFSET(FrameBuffer, mem_addr_);
    for (int k = 0; k < 10; ++k) {
        struct FrameBuffer* fb = (struct FrameBuffer*) ((char*)mem_ptr + fb_offset);
        fb->id_ = k;
        fb->dma_addr_ = ef_memreg_dma_addr(&memreg, fb_offset + payload_offset);
        fb_offset += 2048;
        if (ef_vi_receive_post(&vi, fb->dma_addr_, fb->id_) != 0) {
            std::cerr << "ef_vi_receive_post() failed" << std::endl;
            return -1;
        }
        pkt_bufs[fb->id_] = fb;
    }
    for (int k = 0; k < 10; ++k) {
        struct FrameBuffer* fb = (struct FrameBuffer*) ((char*)mem_ptr + fb_offset);
        fb->id_ = k + 10;
        fb->dma_addr_ = ef_memreg_dma_addr(&memreg, fb_offset + payload_offset);
        fb_offset += 2048;
        pkt_bufs[fb->id_] = fb;
    }

    ef_filter_spec      filter;
    ef_filter_spec_init(&filter, EF_FILTER_FLAG_NONE);
    if (ef_filter_spec_set_ip4_local(
        &filter, IPPROTO_TCP,
        htonl(inet_addr(LOCAL_ADDR)),
        htons(LOCAL_PORT)) != 0) {
        std::cerr << "ef_filter_spec_set_ip4_local() failed" << std::endl;
        return -1;
    }
    if (ef_vi_filter_add(
        &vi,
        handle,
        &filter,
        nullptr) != 0) {
        std::cerr << "ef_vi_filter_add() failed" << std::endl;
        return -1;
    }

    ef_event event_tab[20];
    while (true) {
        int nr_event = ef_eventq_poll(&vi, event_tab, sizeof(event_tab));
        for (int k = 0; k < nr_event; ++k) {
            switch (EF_EVENT_TYPE(event_tab[k])) {
                /** Good data was received. */
                case EF_EVENT_TYPE_RX:
                {
                    auto id = event_tab[k].rx.rq_id;
                    EthHeader* eth_hdr = (EthHeader*) pkt_bufs[id]->mem_addr_;
                    IP4Header* ip4_hdr = (IP4Header*) ((char*) pkt_bufs[id]->mem_addr_ + 14);
                    TCPHeader* tcp_hdr = (TCPHeader*) (ip4_hdr + 1);
                    auto ctx = NetContext::GetContext();
                    FILE* fp = ctx->GetDumpFP();
                    if (fp) {
                        eth_hdr->Dump(fp);
                        ip4_hdr->Dump(fp);
                        tcp_hdr->Dump(fp);
                    }
                    
                    std::cout << "dst mac";
                    for (int i = 0; i < 6; ++i) {
                        std::cout << ":" << std::to_string(eth_hdr->dst_ethaddr_[i]);
                    }
                    std::cout << ";" << std::endl;
                    std::cout << "src mac";
                    for (int i = 0; i < 6; ++i) {
                        std::cout << ":" << std::to_string(eth_hdr->src_ethaddr_[i]);
                    }
                    std::cout << ";" << std::endl;
                    if (ntohs(eth_hdr->be_protocol_) == static_cast<uint16_t>(PPT::IPv4)) {
                        std::cout << "payload protocol = IPv4" << std::endl;
                    } else if (ntohs(eth_hdr->be_protocol_) == static_cast<uint16_t>(PPT::ARP)) {
                        std::cout << "payload protocol = ARP" << std::endl;
                    } else {
                        std::cout << "payload protocol = UNKNOWN" << std::endl;
                    }

                    uint32_t src_addr_val = ntohl(ip4_hdr->be_laddr_);
                    uint32_t dst_addr_val = ntohl(ip4_hdr->be_raddr_);
                    char src_addr[64] { };
                    char dst_addr[64] { };
                    if (inet_ntop(AF_INET, &src_addr_val, src_addr, sizeof(src_addr)) == nullptr) {
                        std::cout << " failed to convert src address: " << src_addr_val << std::endl;
                    }
                    if (inet_ntop(AF_INET, &dst_addr_val, dst_addr, sizeof(dst_addr)) == nullptr) {
                        std::cout << " failed to convert dst address: " << dst_addr_val << std::endl;
                    }
                    std::cout << "src address: value = " << src_addr_val << "; rep = " << src_addr << ";" << std::endl;
                    std::cout << "dst address: value = " << dst_addr_val << "; rep = " << dst_addr << ";" << std::endl;
                    if (ntohs(ip4_hdr->protocol_) == static_cast<uint8_t>(PPT::TCP)) {
                        std::cout << "payload protocol = TCP" << std::endl;
                    } else if (ntohs(ip4_hdr->protocol_) == static_cast<uint8_t>(PPT::UDP)) {
                        std::cout << "payload protocol = UDP" << std::endl;
                    } else {
                        std::cout << "payload protocol = UNKNOWN" << std::endl;
                    }
                }
            }
        }
    }

    return 0;
}